package database

import (
	"abdis/aof"
	"abdis/config"
	"abdis/interface/database"
	"abdis/interface/redis"
	"abdis/lib/logger"
	"abdis/redis/protocol"
	"fmt"
	"runtime/debug"
	"strconv"
	"strings"
	"sync/atomic"
)

type Server struct {
	dbSet []*atomic.Value // *DB

	persister *aof.Persister
}

func (server *Server) selectDB(dbIndex int) (*DB, *protocol.StandardErrReply) {
	if dbIndex < 0 || dbIndex >= len(server.dbSet) {
		return nil, protocol.MakeErrReply("ERR DB index is out of range")
	}
	return server.dbSet[dbIndex].Load().(*DB), nil
}

func makeSimpleServer() *Server {
	server := &Server{}

	server.dbSet = make([]*atomic.Value, config.Properties.Databases)
	for i := range server.dbSet {
		holder := &atomic.Value{}
		holder.Store(makeDB(i))
		server.dbSet[i] = holder
	}
	return server
}

func MakeStandaloneServer() *Server {
	server := makeSimpleServer()

	if config.Properties.AppendOnly {
		aofHandler, err := aof.MakePersister(server, "appendonly.aof", aof.FsyncAlways, func() database.DB {
			return makeSimpleServer()
		})
		if err != nil {
			panic(err)
		}
		server.bindPersister(aofHandler)
	}

	return server
}

func (server *Server) Close() error {
	return nil
}

func (server *Server) AfterClientClose(c redis.Connection) {

}

func (server *Server) ForEach(index int, consumer database.DbConsumer) {
	server.mustSelectDB(index).ForEach(consumer)
}

func (server *Server) Exec(c redis.Connection, cmdLine [][]byte) (result redis.Reply) {
	defer func() {
		if err := recover(); err != nil {
			logger.Warn(fmt.Sprintf("error occurs: %v\n%s", err, string(debug.Stack())))
			result = protocol.MakeUnknownErrReply()
		}
	}()

	reply := server.execSpecialCommand(c, cmdLine)
	if reply != nil {
		return reply
	}

	selectedDB, errReply := server.selectDB(c.GetDBIndex())
	if errReply != nil {
		return errReply
	}

	reply = selectedDB.Exec(c, cmdLine)

	return reply
}

func (server *Server) execSpecialCommand(c redis.Connection, cmdLine [][]byte) redis.Reply {
	name := strings.ToLower(string(cmdLine[0]))
	if name != "ping" {
		logger.Info("cmd is : " + strings.ToUpper(name))
	}

	// 开始做安全校验
	if name != "auth" && name != "ping" && !c.IsAuthed() {
		return protocol.MakeErrReply("ERR not auth")
	}

	switch name {
	case "ping":
		return Ping(cmdLine[1:])
	case "info":
		return Info(server, cmdLine[1:])
	case "auth":
		return Auth(c, cmdLine[1:])
	case "dbsize":
		return DBSize(c, server)
	}

	if c != nil && !c.InMultiState() {
		switch name {
		case "select":
			if len(cmdLine) != 2 {
				return protocol.MakeArgNumErrReply("select")
			}
			return execSelect(c, server, cmdLine[1:])
		case "rewriteaof":
			if config.Properties.AppendOnly {
				return RewriteAOF(server)
			}
		}
	}

	return nil
}

func (server *Server) mustSelectDB(index int) *DB {
	db, err := server.selectDB(index)
	if err != nil {
		panic(err)
	}
	return db
}

func execSelect(c redis.Connection, db *Server, args [][]byte) redis.Reply {
	index, err := strconv.Atoi(string(args[0]))
	if err != nil {
		return protocol.MakeErrReply("ERR invalid DB index")
	}
	if index >= len(db.dbSet) || index < 0 {
		return protocol.MakeErrReply("ERR DB index is out of range")
	}
	c.SelectDB(index)
	return protocol.MakeOkReply()
}

func (server *Server) GetDBSize(index int) (int, int) {
	db := server.mustSelectDB(index)
	return db.data.Len(), db.ttlMap.Len()
}

func (server *Server) bindPersister(handler *aof.Persister) {
	server.persister = handler

	for _, db := range server.dbSet {
		single := db.Load().(*DB)
		single.addAof = func(line database.CmdLine) {
			server.persister.SaveCmd(single.index, line)
		}
	}
}
